﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.ComponentModel.Design;
using System.Diagnostics;
using System.Drawing;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Workflow.Activities;
using System.Workflow.Activities.Rules;
using System.Workflow.ComponentModel;
using System.Workflow.ComponentModel.Compiler;
using System.Workflow.ComponentModel.Design;
using System.Workflow.ComponentModel.Serialization;
using System.Workflow.Runtime;
using Microsoft.ResourceManagement.Utilities;
using Microsoft.ResourceManagement.WebServices;
using Microsoft.ResourceManagement.WebServices.WSResourceManagement;
using Microsoft.ResourceManagement.Workflow.Activities;

namespace Ensynch.ILM2.Workflow.Activities
{
    public partial class UpdateAttributeActivity : SequenceActivity
    {
        public RequestType CurrentRequest;
        public Guid DestinationId;
        public ResourceType DestinationResource;
        public String NewResolvedExpression;
        public String ExistingResolvedExpression;

        private bool destinationIsAResource;
        private bool attributeHasChanged;
        private string attributeName;
        private string attributeType;

        public UpdateAttributeActivity()
        {
            InitializeComponent();
        }

        #region Dependency Properties

        public static DependencyProperty TheActivityNameProperty =
            DependencyProperty.Register("TheActivityName", typeof(string), typeof(UpdateAttributeActivity));

        [Browsable(true)]
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
        [Category("Properties")]
        public string TheActivityName
        {
            get
            {
                object test = this.GetValue(UpdateAttributeActivity.TheActivityNameProperty);
                return test == null ? "" : test.ToString();
            }
            set
            {
                this.SetValue(UpdateAttributeActivity.TheActivityNameProperty, value);
            }
        }

        public static DependencyProperty DestinationStringProperty =
                    DependencyProperty.Register("DestinationString", typeof(string), typeof(UpdateAttributeActivity));

        [Browsable(true)]
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
        [Category("Properties")]
        public string DestinationString
        {
            get
            {
                object test = this.GetValue(UpdateAttributeActivity.DestinationStringProperty);
                return test == null ? "" : test.ToString();
            }
            set
            {
                this.SetValue(UpdateAttributeActivity.DestinationStringProperty, value);
            }
        }

        public static DependencyProperty DestinationValueProperty =
                    DependencyProperty.Register("DestinationValue", typeof(string), typeof(UpdateAttributeActivity));

        [Browsable(true)]
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
        [Category("Properties")]
        public string DestinationValue
        {
            get
            {
                object test = this.GetValue(UpdateAttributeActivity.DestinationValueProperty);
                return test == null ? "" : test.ToString();
            }
            set
            {
                this.SetValue(UpdateAttributeActivity.DestinationValueProperty, value);
            }
        }

        public static DependencyProperty ValueTypeProperty =
                    DependencyProperty.Register("ValueType", typeof(string), typeof(UpdateAttributeActivity));

        [Browsable(true)]
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
        [Category("Properties")]
        public string ValueType
        {
            get
            {
                object test = this.GetValue(UpdateAttributeActivity.ValueTypeProperty);
                return test == null ? "String" : test.ToString();
            }
            set
            {
                this.SetValue(UpdateAttributeActivity.ValueTypeProperty, value);
            }
        }

        #endregion

        #region Non-dependency Properties

        private SequentialWorkflow parentWorkflow;
        public SequentialWorkflow ParentWorkflow
        {
            get
            {
                if (parentWorkflow == null)
                {
                    if (!SequentialWorkflow.TryGetContainingWorkflow(this, out parentWorkflow))
                    {
                        throw new InvalidOperationException("Unable to get Containing Workflow");
                    }
                }
                return parentWorkflow;
            }
        }

        private Guid? requestor;
        public Guid Requestor
        {
            get
            {
                if (requestor == null)
                {
                    requestor = ParentWorkflow.ActorId;
                }
                return (Guid)requestor;
            }
        }

        private Dictionary<string, object> workflowDictionary;
        public Dictionary<string, object> WorkflowDictionary
        {
            get
            {
                if (workflowDictionary == null)
                {
                    workflowDictionary = ParentWorkflow.WorkflowDictionary;
                }
                return workflowDictionary;
            }
        }

        #endregion

        #region Class Methods

        private void _processInput(object sender, EventArgs e)
        {
            char[] delimiters = { '/' };
            string[] destinationTokens = DestinationString
                .Replace('[', delimiters[0]).Replace(']', delimiters[0])
                .Split(delimiters, StringSplitOptions.RemoveEmptyEntries);

            if (destinationTokens.Length < 2)
            {
                throw new ArgumentException("Unsupported destination string: " + DestinationString);
            }

            // Extract the attribute name from the destination string.
            attributeName = destinationTokens.Last();

            if (DestinationString.IndexOf("//Requestor/") >= 0)
            {
                DestinationId = new Guid(Requestor.ToString(null));
                destinationIsAResource = true;
            }
            else if (DestinationString.IndexOf("//Target/") >= 0)
            {
                DestinationId = CurrentRequest.Target.GetGuid();
                destinationIsAResource = true;
            }
            else if (DestinationString.IndexOf("//Request/") >= 0 ||
                DestinationString.IndexOf("//WorkflowData/") >= 0)
            {
                /// If we're writing to WorkflowData or Request, then the destination 
                /// is not a resource, and we just have to write a key/value pair to 
                /// the destination, instead of doing a resource update.
                destinationIsAResource = false;
            }
            else
            {
                throw new ArgumentException("Unsupported destination string: " + DestinationString);
            }
        }

        private void _destinationIsAResource(object sender, ConditionalEventArgs e)
        {
            e.Result = destinationIsAResource;
        }

        private void _destinationIsNotAResource(object sender, ConditionalEventArgs e)
        {
            e.Result = !destinationIsAResource;
        }

        private void _processExistingResourceValue(object sender, EventArgs e)
        {
            foreach (KeyValuePair<string, ResourcePropertyInfo> property in
                DestinationResource.ResourceProperties)
            {
                if (property.Value.Name.Equals(attributeName))
                {
                    attributeType = property.Value.Type;
                    break;
                }
            }

            try
            {
                ExistingResolvedExpression =
                    printResourceProperty(DestinationResource[attributeName]);
            }
            catch { }
        }

        private string printResourceProperty(object obj)
        {
            try
            {
                if (obj.GetType() == typeof(UniqueIdentifier))
                {
                    return ((UniqueIdentifier)obj).Value;
                }
            }
            catch { }

            return obj.ToString();
        }

        private void _processExistingResolvedValue(object sender, EventArgs e)
        {
            switch (ValueType)
            {
                case "Boolean":
                    attributeType = "booleanType";
                    break;
                case "DateTime":
                    attributeType = "dateTimeType";
                    break;
                case "Reference":
                    attributeType = "referenceType";
                    break;
                case "Integer":
                    attributeType = "integerType";
                    break;
                case "String":
                default:
                    attributeType = "stringType";
                    break;
            }
        }
        // Method names must be case-insensitively different from activity names.
        private void _processOutput(object sender, EventArgs e)
        {
            attributeHasChanged = false;

            // Error checking on the attribute type.
            if (attributeType == null)
            {
                throw new ArgumentException("Could not resolve destination attribute: " + attributeName);
            }

            // We'll resolve the expression further for some types (e.g. dates).
            object original = parse(ExistingResolvedExpression);
            object final = parse(NewResolvedExpression);

            // Compare the original/final values.
            attributeHasChanged = !theseAreEqual(original, final);

            // Finally, update the destination if the attribute has changed.
            if (attributeHasChanged)
            {
                if (!destinationIsAResource)
                {
                    if (DestinationString.IndexOf("//WorkflowData/") >= 0)
                    {
                        updateWorkflowData(final);
                    }
                    else if (DestinationString.IndexOf("//Request/") >= 0)
                    {
                        updateRequest(final);
                    }
                }
                else
                {
                    UpdateDestination.UpdateParameters = new UpdateRequestParameter[]
                    {
                        new UpdateRequestParameter(attributeName,
                        UpdateMode.Modify, final)
                    };
                }
            }
        }

        private bool theseAreEqual(object obj1, object obj2)
        {
            bool equal = false;
            string str1 = obj1 == null ? null : obj1.ToString();
            string str2 = obj2 == null ? null : obj2.ToString();

            // First check if both new & existing are null or empty.
            // These are considered equal.
            if (string.IsNullOrEmpty(str1) &&
                string.IsNullOrEmpty(str2))
            {
                equal = true;
            }
            else if (string.IsNullOrEmpty(str1) &&
                !string.IsNullOrEmpty(str2))
            {
                equal = false;
            }
            else if (!string.IsNullOrEmpty(str1) &&
                string.IsNullOrEmpty(str2))
            {
                equal = false;
            }
            else
            {
                equal = str1.Equals(str2);
            }

            return equal;
        }

        private object parse(string input)
        {
            object parsed = null;
            switch (attributeType)
            {
                case "booleanType":
                    parsed = parseBoolean(input);
                    break;
                case "dateTimeType":
                    parsed = parseDateTime(input);
                    break;
                case "integerType":
                    parsed = parseInteger(input);
                    break;
                case "referenceType":
                    parsed = parseReference(input);
                    break;
                case "binaryType":
                case "stringType":
                default:
                    parsed = input;
                    break;
            }
            return parsed;
        }

        private string parseReference(string input)
        {
            return input == null ? null : input.ToLower().Replace("urn:uuid:", "");
        }

        private DateTime? parseDateTime(string input)
        {
            DateTime? result = null;

            // By design, it's not an error to pass a null or empty value,
            // but don't try to parse it.
            if (string.IsNullOrEmpty(input))
            {
                return result;
            }

            // Parse the date with the current culture.
            CultureInfo format = CultureInfo.CurrentCulture;

            try
            {
                char[] delimiters = new char[]
                {
                    '+', '-'
                };
                string[] tokens = input.Split(delimiters, StringSplitOptions.RemoveEmptyEntries);
                string[] operators = input.Split(tokens, StringSplitOptions.RemoveEmptyEntries);

                if (tokens.Length != operators.Length + 1)
                {
                    throw new Exception();
                }

                // Parse each DateTime token, convert it to ticks, and add/subtract
                // to the total.
                long total = tryParseDateOrTime(tokens[0].Trim(), format);
                for (int i = 1; i < tokens.Length; i++ )
                {
                    long token = tryParseDateOrTime(tokens[i].Trim(), format);
                    total = simpleMath(total, token, operators[i - 1].Trim());
                }

                // The answer is now in units of 100 nanoseconds (1e-9 s).
                // Cast it back to a DateTime.
                DateTime answer = new DateTime(total);
                result = answer;
            }
            catch
            {
                string msg =
                    "Error evaluating datetime expression: " + input;
                throw new ArgumentException(msg);
            }

            return result;
        }

        private long simpleMath(long left, long right, string op)
        {
            switch (op)
            {
                case "+":
                    return left + right;
                    break;
                case "-":
                    return left - right;
                    break;
                default:
                    throw new Exception();
                    break;
            }
        }

        /// <summary>
        /// Trys to parse arguments as (1st) number of days, (2nd) a TimeSpan, or
        /// (3rd) a DateTime.
        /// </summary>
        /// <returns>
        /// If successful, returns the TimeSpan or DateTime as the number of ticks
        /// (number of 100 nanoseconds, 1e-9 seconds).
        /// </returns>
        private long tryParseDateOrTime(string input, IFormatProvider format)
        {
            long result;

            try
            {
                // Try to parse an integer number of days first.
                int days;
                if (int.TryParse(input, out days))
                {
                    TimeSpan ts = new TimeSpan(days, 0, 0, 0);
                    result = ts.Ticks;
                    return result;
                }
            }
            catch { }
            try
            {
                // Then try to parse a timespan ([dd.]HH:mm:ss[.000])
                TimeSpan ts;
                if (TimeSpan.TryParse(input, out ts))
                {
                    result = ts.Ticks;
                    return result;
                }
            }
            catch { }
            try
            {
                // Finally, try to parse a datetime string.
                DateTime dt;
                if (DateTime.TryParse(input, format, DateTimeStyles.None, out dt))
                {
                    result = dt.Ticks;
                    return result;
                }
            }
            catch { }

            throw new Exception();
        }

        private bool? parseBoolean(string input)
        {
            bool? result = null;

            // By design, it's not an error to pass a null or empty value,
            // but don't try to parse it.
            if (string.IsNullOrEmpty(input))
            {
                return result;
            }

            // Cast it to a bool.
            bool test;
            if (bool.TryParse(input, out test))
            {
                result = test;
            }
            else
            {
                string msg =
                    "Error parsing boolean expression: " + input;
                throw new ArgumentException(msg);
            }

            return result;
        }

        private int? parseInteger(string input)
        {
            int? result = null;

            // By design, it's not an error to pass a null or empty value,
            // but don't try to parse it.
            if (string.IsNullOrEmpty(input))
            {
                return result;
            }

            int test;
            if (int.TryParse(input, out test))
            {
                result = test;
            }
            else
            {
                string msg =
                    "Error parsing integer expression: " + input;
                throw new ArgumentException(msg);
            }

            return result;
        }

        private void updateWorkflowData(object final)
        {
            if (!WorkflowDictionary.ContainsKey(attributeName))
            {
                WorkflowDictionary.Add(attributeName, final);
            }
            else
            {
                WorkflowDictionary[attributeName] = final;
            }
        }

        private void updateRequest(object final)
        {
            if (!CurrentRequest.ResourceProperties.ContainsKey(attributeName))
            {
                CurrentRequest.ResourceProperties.Add(attributeName,
                    new ResourcePropertyInfo(attributeName, attributeType, false, false));
                CurrentRequest[attributeName] = final;
            }
            else
            {
                CurrentRequest[attributeName] = final;
            }
        }

        private void _resourceHasChanged(object sender, ConditionalEventArgs e)
        {
            e.Result = destinationIsAResource && attributeHasChanged;
        }

        #endregion

    }
}
